package calc

abstract class Calculator extends Arithmetic {
  def nextNumber(s: String): Option[String]

  def asComputable(token: Token[T]): Computable[T] =
    token match {
      case Plus() => Computable(plus)
      case Minus() => Computable(minus)
      case UMinus() => Computable(uminus)
      case Multiply() => Computable(multiply)
      case Divide() => Computable(divide)
      case Number(x) => Computable(number(x))
    }

  def calculate(in: String): T = {
    val tokensInInfix = tokenize(in.toList)
    val tokensInPostfix = infix2postfix(tokensInInfix)
    val computables = tokensInPostfix.map(asComputable(_))
    compute(computables)
  }

  def compute[T](computables: List[Computable[T]]) =
    computables.foldLeft(List[T]())((s: List[T], c: Computable[T]) => c.compute(s)).head

  def tokenize(chars: List[Char], uminusPossible: Boolean = true): List[Token[T]] = {
    object NumberExtract {
      def unapply(c: List[Char]): Option[(String, List[Char])] = nextNumber(c.mkString).map((n: String) => (n, c.drop(n.length)))
    }
    
    chars match {
      case List() => List()
      case NumberExtract(number, tail) => Number[T](number) :: tokenize(tail, false)
      case a :: tail if (a.isWhitespace) => tokenize(tail, uminusPossible)
      case '+' :: tail => Plus[T]() :: tokenize(tail, false)
      case '-' :: tail => (if (uminusPossible) UMinus[T]() else Minus[T]()) :: tokenize(tail, false)
      case '*' :: tail => Multiply[T]() :: tokenize(tail, false)
      case '/' :: tail => Divide[T]() :: tokenize(tail, false)
      case '(' :: tail => OpenParen[T]() :: tokenize(tail, true)
      case ')' :: tail => CloseParen[T]() :: tokenize(tail, false)
    }
  }

  def infix2postfix(tokens: List[Token[T]], stack: List[Token[T]] = List[Token[T]]()): List[Token[T]] =
    tokens match {
      case List() => stack
      case (n @ Number(x)) :: tail => n :: infix2postfix(tail, stack)
      case OpenParen() :: tail => infix2postfix(tail, OpenParen[T]() :: stack)
      case CloseParen() :: tail => stack.takeWhile(_ != OpenParen()) ::: infix2postfix(tail, stack.dropWhile(_ != OpenParen()).tail)
      case (op @ Operator()) :: tail => stack.takeWhile(op < _) ::: infix2postfix(tail, op :: stack.dropWhile(op < _))
    }
}
